<?php
/**
 * Class used internally by Diff to actually compute the diffs.
 *
 * This class uses the Unix `diff` program via shell_exec to compute the
 * differences between the two input arrays.
 *
 * $Horde: framework/Text_Diff/Diff/Engine/shell.php,v 1.6.2.3 2008/01/04 10:37:27 jan Exp $
 *
 * Copyright 2007-2008 The Horde Project (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (LGPL). If you did
 * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
 *
 * @author  Milian Wolff <mail@milianw.de>
 * @package Text_Diff
 * @since   0.3.0
 */
class Text_Diff_Engine_shell {

	/**
	 * Path to the diff executable
	 *
	 * @var string
	 */
	var $_diffCommand = 'diff';

	/**
	 * Returns the array of differences.
	 *
	 * @param array $from_lines lines of text from old file
	 * @param array $to_lines   lines of text from new file
	 *
	 * @return array all changes made (array with Text_Diff_Op_* objects)
	 */
	function diff($from_lines, $to_lines)
	{
		array_walk($from_lines, array('Text_Diff', 'trimNewlines'));
		array_walk($to_lines, array('Text_Diff', 'trimNewlines'));

		$temp_dir = Text_Diff::_getTempDir();

		// Execute gnu diff or similar to get a standard diff file.
		$from_file = tempnam($temp_dir, 'Text_Diff');
		$to_file = tempnam($temp_dir, 'Text_Diff');
		$fp = fopen($from_file, 'w');
		fwrite($fp, implode("\n", $from_lines));
		fclose($fp);
		$fp = fopen($to_file, 'w');
		fwrite($fp, implode("\n", $to_lines));
		fclose($fp);
		$diff = shell_exec($this->_diffCommand . ' ' . $from_file . ' ' . $to_file);
		unlink($from_file);
		unlink($to_file);

		if (is_null($diff)) {
			// No changes were made
			return array(new Text_Diff_Op_copy($from_lines));
		}

		$from_line_no = 1;
		$to_line_no = 1;
		$edits = array();

		// Get changed lines by parsing something like:
		// 0a1,2
		// 1,2c4,6
		// 1,5d6
		preg_match_all('#^(\d+)(?:,(\d+))?([adc])(\d+)(?:,(\d+))?$#m', $diff,
		$matches, PREG_SET_ORDER);

		foreach ($matches as $match) {
			if (!isset($match[5])) {
				// This paren is not set every time (see regex).
				$match[5] = false;
			}

			if ($match[3] == 'a') {
				$from_line_no--;
			}

			if ($match[3] == 'd') {
				$to_line_no--;
			}

			if ($from_line_no < $match[1] || $to_line_no < $match[4]) {
				// copied lines
				assert('$match[1] - $from_line_no == $match[4] - $to_line_no');
				array_push($edits,
				new Text_Diff_Op_copy(
				$this->_getLines($from_lines, $from_line_no, $match[1] - 1),
				$this->_getLines($to_lines, $to_line_no, $match[4] - 1)));
			}

			switch ($match[3]) {
				case 'd':
					// deleted lines
					array_push($edits,
					new Text_Diff_Op_delete(
					$this->_getLines($from_lines, $from_line_no, $match[2])));
					$to_line_no++;
					break;

				case 'c':
					// changed lines
					array_push($edits,
					new Text_Diff_Op_change(
					$this->_getLines($from_lines, $from_line_no, $match[2]),
					$this->_getLines($to_lines, $to_line_no, $match[5])));
					break;

				case 'a':
					// added lines
					array_push($edits,
					new Text_Diff_Op_add(
					$this->_getLines($to_lines, $to_line_no, $match[5])));
					$from_line_no++;
					break;
			}
		}

		if (!empty($from_lines)) {
			// Some lines might still be pending. Add them as copied
			array_push($edits,
			new Text_Diff_Op_copy(
			$this->_getLines($from_lines, $from_line_no,
			$from_line_no + count($from_lines) - 1),
			$this->_getLines($to_lines, $to_line_no,
			$to_line_no + count($to_lines) - 1)));
		}

		return $edits;
	}

	/**
	 * Get lines from either the old or new text
	 *
	 * @access private
	 *
	 * @param array &$text_lines Either $from_lines or $to_lines
	 * @param integer &$line_no Current line number
	 * @param integer $end Optional end line, when we want to chop more than one line.
	 * @return array The chopped lines
	 */
	function _getLines(&$text_lines, &$line_no, $end = false)
	{
		if (!empty($end)) {
			$lines = array();
			// We can shift even more
			while ($line_no <= $end) {
				array_push($lines, array_shift($text_lines));
				$line_no++;
			}
		} else {
			$lines = array(array_shift($text_lines));
			$line_no++;
		}

		return $lines;
	}

}
